Skip to content

feat(input-area): add auto resize support#625

Merged
mattrothenberg merged 4 commits into
cloudflare:mainfrom
nocdn:feat/input-area-auto-resize
Jul 10, 2026
Merged

feat(input-area): add auto resize support#625
mattrothenberg merged 4 commits into
cloudflare:mainfrom
nocdn:feat/input-area-auto-resize

Conversation

@nocdn

@nocdn nocdn commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an optional autoResize prop to InputArea and its Textarea alias. When enabled, the textarea grows vertically with its content.

Changes

  • Resize the textarea on initial render and value changes
  • Preserve onChange, onValueChange, and forwarded ref behavior
  • Hide the manual resize handle while automatic resizing is enabled
  • Add unit tests and a documentation example
  • Add a minor changeset for @cloudflare/kumo

Implementation note

field-sizing: content is used as a progressive enhancement for the server-rendered, pre-hydration frame. w-full constrains its inline size so it only grows vertically. After hydration, the scrollHeight measurement applies the explicit height and enforces minRows and maxRows; browsers without field-sizing support use the same JavaScript path as a fallback.

Testing

  • CI=true pnpm --filter @cloudflare/kumo exec vitest run --project=unit src/components/input/input.test.tsx
  • pnpm --filter @cloudflare/kumo typecheck
  • pnpm --filter @cloudflare/kumo lint

  • Reviews
    • bonk has reviewed the change
    • automated review not possible because: this focused component enhancement will receive normal PR review
  • Tests
    • Tests included/updated
    • Automated tests not possible - manual testing has been completed as follows:
    • Additional testing not necessary because:

Bartek Bak and others added 2 commits July 10, 2026 10:55
- No-op entirely when autoResize is off so a user's manual drag-resize
  is never clobbered by value changes; restore inline styles when the
  prop toggles off
- Add maxRows: clamp growth via computed line-height and flip
  overflow-y to auto past the clamp so content stays scrollable
- Account for box-sizing and borders when converting scrollHeight to
  an explicit height
- Re-measure on inline-size changes via ResizeObserver so rewrapped
  content keeps the correct height
- Avoid the React 18 SSR useLayoutEffect warning and honor React 19
  callback-ref cleanups

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mattrothenberg

Copy link
Copy Markdown
Collaborator

/bonk please review

@pkg-pr-new

pkg-pr-new Bot commented Jul 10, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@cloudflare/kumo@625

commit: d26c469

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Docs Preview

View docs preview

Commit: d26c469

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
Visual Regression Report — 20 changed, 21 unchanged

20 screenshot(s) with visual changes:

Button / Variant: Secondary Destructive

1,122 px (1.11%) changed

Before After Diff
Before After Diff

Button / Loading State

16 px (0.02%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Actions

363 px (0.36%) changed

Before After Diff
Before After Diff

Dialog / Dialog Basic

316 px (0.31%) changed

Before After Diff
Before After Diff

Dialog / Dialog Sizes

2,303 px (2.27%) changed

Before After Diff
Before After Diff

Dialog / Dialog Alert

1,635 px (1.61%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Select

480 px (0.47%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Combobox

383 px (0.38%) changed

Before After Diff
Before After Diff

Dialog / Dialog With Dropdown

383 px (0.38%) changed

Before After Diff
Before After Diff

Dialog (Open)

0 px (0%) changed

Before After Diff
Before After Diff

Select / Select Sizes

2,690 px (1.45%) changed

Before After Diff
Before After Diff

Select / Select With Description

1,688 px (1.43%) changed

Before After Diff
Before After Diff

Select / Select Loading

0 px (0%) changed

Before After Diff
Before After Diff

Select / Select Multiple

1,265 px (1.25%) changed

Before After Diff
Before After Diff

Select / Select Complex

1,279 px (1.08%) changed

Before After Diff
Before After Diff

Select / Select Disabled Items

785 px (0.77%) changed

Before After Diff
Before After Diff

Select / Select Grouped

813 px (0.8%) changed

Before After Diff
Before After Diff

Select / Select Grouped With Disabled

1,414 px (1.39%) changed

Before After Diff
Before After Diff

Select / Select Long List

1,517 px (1.28%) changed

Before After Diff
Before After Diff

Select (Open)

541 px (0%) changed

Before After Diff
Before After Diff
21 screenshot(s) unchanged
  • Button / Basic
  • Button / Variant: Primary
  • Button / Variant: Secondary
  • Button / Variant: Ghost
  • Button / Variant: Destructive
  • Button / Variant: Outline
  • Button / Sizes
  • Button / With Icon
  • Button / Icon Only
  • Button / Disabled State
  • Button / Title
  • Button / Link as Button
  • Dialog / Dialog Confirmation
  • Dialog / Custom Max Width
  • Select / Select Basic
  • Select / Select Without Label
  • Select / Select With Error
  • Select / Select Placeholder
  • Select / Select With Tooltip
  • Select / Select Custom Rendering
  • Select / Select Disabled Options

Generated by Kumo Visual Regression

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough PR! The implementation is solid — useTextareaAutoResize correctly accounts for box-sizing, borders, and width-driven rewraps. The React 19 callback-ref cleanup and SSR-safe useIsomorphicLayoutEffect are nice touches.

A couple of suggestions and one minor concern:

Performance: handleChange calls resize() on every input event, and the layout effect calls it again after the subsequent render. For controlled inputs this causes two forced synchronous layouts per keystroke. Relying solely on the layout effect for controlled components would halve that work. You could detect control mode (e.g. 'value' in inputProps) and skip the handleChange resize when controlled, since the layout effect already covers post-render measurement.

Edge case: getComputedStyle will typically resolve line-height to a pixel value, but if a browser or testing environment returns a unitless string like "1.5", parsePx("1.5") yields 1.5 and maxHeight collapses to 1.5 * maxRows pixels, which would clamp far too aggressively. A small safety check like Number.isNaN(parsePx(style.lineHeight)) ? parsePx(style.fontSize) * 1.2 : parsePx(style.lineHeight) would make the fallback more explicit.

Tests: Great coverage for the main paths. Consider adding a test that unmounts an autoResize textarea and verifies style.height and style.overflowY are cleared (the cleanup is implemented but not asserted).

None of these are blockers — the feature works correctly as-is. Nice work.

Comment thread packages/kumo/src/components/input/input-area.tsx Outdated
Comment thread packages/kumo/src/components/input/input-area.tsx Outdated
@ask-bonk

ask-bonk Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review submitted to PR #625.

Summary: The auto-resize implementation is solid with good edge-case handling (box-sizing, borders, width rewraps, React 19 refs, SSR). I left two non-blocking inline suggestions:

  1. Performanceresize() in handleChange + the post-render layout effect causes double forced layouts per keystroke for controlled inputs. Skipping the handleChange resize when controlled would eliminate the redundancy.
  2. Robustness — The line-height parsing assumes getComputedStyle always returns a pixel value. If any environment returns a unitless string, the maxRows clamp would be far too aggressive.

Overall approved with comments — no blockers.

github run

…ht parsing

- Skip the change-handler resize for controlled inputs; the post-render
  layout effect already re-measures, so this halves forced layouts per
  keystroke
- Treat a unitless computed line-height (e.g. "1.5") as a font-size
  multiplier instead of pixels so the maxRows clamp stays correct
- Add tests for unitless line-height and unmount style cleanup

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mattrothenberg

Copy link
Copy Markdown
Collaborator

/bonk I think I addressed your feedback! Please re-review

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All prior feedback has been addressed. Re-approving.

  • Performance: handleChange now guards resize() with !isControlled, eliminating the double forced synchronous layout per keystroke for controlled inputs.
  • Edge case: line-height parsing now correctly treats unitless values (e.g. '1.5') as font-size multipliers instead of raw pixels, and falls back to fontSize * 1.2 for 'normal'.
  • Tests: Added the unmount cleanup assertion verifying style.height and style.overflowY are cleared.

No blockers — nice work!

@ask-bonk

ask-bonk Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Re-reviewed and approved

All three items from the prior review were addressed:

  1. PerformancehandleChange now only calls resize() for uncontrolled inputs (!isControlled), avoiding the double forced synchronous layout on every keystroke for controlled components.
  2. Edge caseline-height parsing now handles unitless values (e.g. "1.5") by multiplying against fontSize, and falls back to fontSize * 1.2 for "normal".
  3. Tests — Added "clears inline styles on unmount" asserting style.height and style.overflowY are reset.

github run

@mattrothenberg

Copy link
Copy Markdown
Collaborator

/bonk review latest changes

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The auto-resize implementation is clean, well-tested, and the prior review feedback has been addressed thoroughly.

One non-blocking nit: the PR description explicitly says field-sizing: content was avoided, but the autoResize className still includes field-sizing-content. Since scrollHeight and explicit height handles resizing fully, the class seems redundant. If it's kept intentionally (e.g. as a progressive enhancement), consider updating the PR description to match.

Comment thread packages/kumo/src/components/input/input-area.tsx
@ask-bonk

ask-bonk Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review submitted to PR #625.

Summary: Approved with one non-blocking suggestion.

The implementation is solid — controlled-input performance, unitless line-height parsing, and unmount cleanup are all handled correctly. I flagged a minor inconsistency between the PR description (which says field-sizing: content was avoided) and the actual code, which still applies the field-sizing-content class. Suggested either removing the redundant class or updating the description.

github run

@mattrothenberg mattrothenberg merged commit b05ea3f into cloudflare:main Jul 10, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants